package br.com.bancodecurriculos.util.dao;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

import br.com.bancodecurriculos.util.dao.SystemObject;

/**
 * 
 * @author Aldo Ramos
 * 
 * @param <T>
 * 
 *            OBS.: Pelo fato do uso da classe estÃ¡ se tornando cada vez mais
 *            complexo, optou-se por nÃ£o utilizÃ¡-la
 * 
 */
public class PreparedStatementFactory {

	private static final String INSERT = "INSERT INTO";
	private static final String VALUES = "VALUES";
	private static final String UPDATE = "UPDATE";
	private static final String SET = "SET";
	private static final String DELETE = "DELETE";
	private static final String SELECT = "SELECT";
	private static final String FROM = "FROM";
	private static final String WHERE = "WHERE";
	private static final String AND = "AND";

	private static final String GET_ID = "getId";

	private static PreparedStatementFactory instance;

	private PreparedStatementFactory() {

	}

	public static PreparedStatementFactory getInstance() {
		if (instance == null) {
			instance = new PreparedStatementFactory();
		}
		return instance;
	}

	/**
	 * Retorna um PreparedStatement para insercao
	 * 
	 * @param t
	 * @return
	 * @throws ClassNotFoundException
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 * @throws InvocationTargetException
	 * @throws SQLException
	 * @throws InstantiationException
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 * 
	 *             TODO tratar caso OneToMany
	 * @throws IOException
	 * 
	 */
	public PreparedStatement getInsertStatement(Object t)
			throws ClassNotFoundException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException, SQLException,
			InstantiationException, NoSuchMethodException, SecurityException,
			IOException {

		HashMap<String, Object> hashMap = this.getAttributesAndValues(t);
		PreparedStatement preparedStatement = JDBCConnectionFactory
				.getInstance().getConnection()
				.prepareStatement(this.generateInsertSQL(t, hashMap));
		return this.setValues(preparedStatement, hashMap);

	}

	/**
	 * Retorna um PreparedStatement para atualizacao
	 * 
	 * @param t
	 * @return
	 * @throws ClassNotFoundException
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 * @throws InvocationTargetException
	 * @throws SQLException
	 * @throws InstantiationException
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 * @throws IOException
	 */
	public PreparedStatement getUpdateStatement(Object t)
			throws ClassNotFoundException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException, SQLException,
			InstantiationException, NoSuchMethodException, SecurityException,
			IOException {

		HashMap<String, Object> hashMap = this.getAttributesAndValues(t);
		PreparedStatement preparedStatement = JDBCConnectionFactory
				.getInstance().getConnection()
				.prepareStatement(this.generateUpdateSQL(t, hashMap));
		return this.setValues(preparedStatement, hashMap);
	}

	/**
	 * Retorna um PreparedStatement para consulta
	 * 
	 * @param t
	 * @param select
	 *            colunas a serem retornadas
	 * @param by
	 *            condicao
	 * @return
	 * @throws SQLException
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	public PreparedStatement getSelectStatement(Class<?> clazz,
			List<String> select, HashMap<String, Object> by)
			throws ClassNotFoundException, SQLException, IOException {

		PreparedStatement preparedStatement = JDBCConnectionFactory
				.getInstance()
				.getConnection()
				.prepareStatement(
						this.genereteSelectStatement(clazz, select, by));
		return this.setValues(preparedStatement, by);
	}

	/**
	 * Retorna um PreparedStatement para remocao
	 * 
	 * @param t
	 * @return
	 * @throws ClassNotFoundException
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 * @throws InvocationTargetException
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 * @throws SQLException
	 * @throws IOException
	 */
	public PreparedStatement getDeleteStatement(Class<?> clazz, Object id)
			throws ClassNotFoundException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException,
			NoSuchMethodException, SecurityException, SQLException, IOException {

		return JDBCConnectionFactory.getInstance().getConnection()
				.prepareStatement(this.generateDeleteSQL(clazz, id));
	}

	/**
	 * Seta os valores dos atributos de um objeto em um PreparedStatement
	 * 
	 * @param preparedStatement
	 * @param hashMap
	 * @return
	 * @throws SQLException
	 * 
	 *             TODO generalizar os setters
	 * 
	 */
	private PreparedStatement setValues(PreparedStatement preparedStatement,
			HashMap<String, Object> hashMap) throws SQLException {
		int count = 1;
		for (Entry<String, Object> entry : hashMap.entrySet()) {
			if (Boolean.class.isAssignableFrom(entry.getValue().getClass())) {
				preparedStatement.setBoolean(count, (Boolean) entry.getValue());
			} else if (Integer.class.isAssignableFrom(entry.getValue()
					.getClass())) {
				preparedStatement.setInt(count, (Integer) entry.getValue());
			} else if (Long.class.isAssignableFrom(entry.getValue().getClass())) {
				preparedStatement.setLong(count, (Long) entry.getValue());
			} else if (Double.class.isAssignableFrom(entry.getValue()
					.getClass())) {
				preparedStatement.setDouble(count, (Double) entry.getValue());
			} else if (String.class.isAssignableFrom(entry.getValue()
					.getClass())) {
				preparedStatement.setString(count, entry.getValue().toString());
			} else if (Date.class.isAssignableFrom(entry.getValue().getClass())) {
				preparedStatement.setDate(count, (Date) entry.getValue());
			}
			count++;
		}
		return preparedStatement;
	}

	/**
	 * Obtem pares atributo, valor de um dado objeto
	 * 
	 * @param t
	 * @return
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 * @throws InvocationTargetException
	 * @throws InstantiationException
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 */
	private HashMap<String, Object> getAttributesAndValues(Object t)
			throws IllegalAccessException, IllegalArgumentException,
			InvocationTargetException, InstantiationException,
			NoSuchMethodException, SecurityException {

		Method[] methods = t.getClass().getDeclaredMethods();
		HashMap<String, Object> hashMap = new HashMap<String, Object>();
		for (Method method : methods) {
			if (this.isGetter(method) && !isGetId(method)
					&& !this.returnAnSystemObject(method)
					&& !this.isReturnArrayOrCollection(method)) {
				hashMap.put(method.getName().substring(3).toLowerCase(),
						method.invoke(t));
			} else if (this.returnAnSystemObject(method)) {
				Object object = method.invoke(t);
				Method method2 = object.getClass().getDeclaredMethod(GET_ID,
						null);
				hashMap.put(
						"id_"
								+ this.getFirtLetterLowerCase(this
										.getName(method.getReturnType()
												.getName())), method2
								.invoke(object));
			}
		}
		return hashMap;
	}

	/**
	 * Gera uma string com o sql para insercao
	 * 
	 * @param t
	 * @param hashMap
	 * @return
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 * @throws InvocationTargetException
	 */
	private String generateInsertSQL(Object t, HashMap<String, Object> hashMap)
			throws IllegalAccessException, IllegalArgumentException,
			InvocationTargetException {

		StringBuilder sql = new StringBuilder();
		sql.append(INSERT).append(" ");
		sql.append(this.getFirtLetterLowerCase(this.getName(t.getClass()
				.getName())));
		sql.append(" ").append("(");
		int count = 0;
		for (Entry<String, Object> entry : hashMap.entrySet()) {
			if (count < hashMap.size() - 1) {
				sql.append(this.getFirtLetterLowerCase(entry.getKey())).append(
						", ");
			} else {
				sql.append(this.getFirtLetterLowerCase(entry.getKey()));
			}
			count++;
		}
		sql.append(") ").append(VALUES).append(" (");
		for (int i = 0; i < hashMap.size(); i++) {
			if (i < hashMap.size() - 1) {
				sql.append("?, ");
			} else {
				sql.append("?");
			}
		}
		sql.append(")");
		return sql.toString();
	}

	/**
	 * Gera uma string com o sql para atualizacao
	 * 
	 * @param t
	 * @param hashMap
	 * @return
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 * @throws InvocationTargetException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	private String generateUpdateSQL(Object t, HashMap<String, Object> hashMap)
			throws NoSuchMethodException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException,
			SecurityException {

		StringBuilder sql = new StringBuilder();
		sql.append(UPDATE).append(" ");
		sql.append(this.getFirtLetterLowerCase(this.getName(t.getClass()
				.getName())));
		sql.append(" ").append(SET).append(" ");
		int count = 0;
		for (Entry<String, Object> entry : hashMap.entrySet()) {
			if (count < hashMap.size() - 1) {
				sql.append(this.getFirtLetterLowerCase(entry.getKey())).append(
						" = ?,");
			} else {
				sql.append(this.getFirtLetterLowerCase(entry.getKey())).append(
						" = ?");
			}
			sql.append(" ");
			count++;
		}
		sql.append(WHERE).append(" id = ");
		sql.append(t.getClass().getDeclaredMethod(GET_ID, null).invoke(t));
		return sql.toString();
	}

	/**
	 * Gera uma string com o sql para remocao
	 * 
	 * @param t
	 * @return
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 * @throws InvocationTargetException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	private String generateDeleteSQL(Class<?> clazz, Object id)
			throws IllegalAccessException, IllegalArgumentException,
			InvocationTargetException, NoSuchMethodException, SecurityException {

		StringBuilder sql = new StringBuilder();
		sql.append(DELETE).append(" ").append(FROM).append(" ");
		sql.append(this.getFirtLetterLowerCase(this.getName(clazz.getName())));
		sql.append(" ").append(WHERE).append(" id = ").append(id.toString());
		return sql.toString();
	}

	/**
	 * Gera uma string com o sql para consulta
	 * 
	 * @param t
	 * @param select
	 * @param hashMap
	 * @return
	 */
	private String genereteSelectStatement(Class<?> clazz, List<String> select,
			HashMap<String, Object> by) {
		StringBuilder sql = new StringBuilder();
		sql.append(SELECT);
		sql.append(" ");
		if (select != null) {
			for (int i = 0; i < select.size(); i++) {
				if (i < select.size() - 1) {
					sql.append(select.get(i)).append(",");
				} else {
					sql.append(select.get(i));
				}
				sql.append(" ");
			}
		} else {
			sql.append("* ");
		}
		sql.append(FROM).append(" ");
		sql.append(this.getFirtLetterLowerCase(this.getName(clazz.getName())));
		sql.append(" ").append(WHERE).append(" ");
		int count = 0;
		for (Entry<String, Object> entry : by.entrySet()) {
			if (count < by.size() - 1) {
				sql.append(entry.getKey()).append(" = ?").append(" ")
						.append(AND).append(" ");
			} else {
				sql.append(entry.getKey()).append(" = ?");
			}
		}
		return sql.toString();
	}

	/**
	 * Retorna o nome um arquivo java a partir do caminho
	 * 
	 * @param path
	 * @return
	 */
	private String getName(String path) {
		String[] packages = path.split("\\.");
		return packages[packages.length - 1];
	}

	/**
	 * Deixa minuscula a primeira letra de uma string
	 * 
	 * @param string
	 * @return
	 */
	private String getFirtLetterLowerCase(String string) {
		return string.substring(0, 1).toLowerCase().concat(string.substring(1));
	}

	/**
	 * Verifica se o retorno do metodo eh um array ou uma instancia de uma
	 * implementacao de Collection
	 * 
	 * @param method
	 * @return
	 */
	private boolean isReturnArrayOrCollection(Method method) {
		if (method.getReturnType().isArray()) {
			return true;
		}
		if (Collection.class.isAssignableFrom(method.getReturnType())) {
			return true;
		}
		return false;
	}

	/**
	 * Verfica se o retorno do metodo eh um objeto de uma classe implementada no
	 * sistema
	 * 
	 * @param method
	 * @return
	 */
	private boolean returnAnSystemObject(Method method) {
		Class<?>[] interfaces = method.getReturnType().getInterfaces();
		if (interfaces.length == 0) {
			return false;
		}
		for (Class<?> clazz : interfaces) {
			if (SystemObject.class.isAssignableFrom(clazz)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Verifica se o metodo retorna o id do objeto
	 * 
	 * @param method
	 * @return
	 */
	private boolean isGetId(Method method) {
		if (method.getName().equals("getId")) {
			return true;
		}
		return false;
	}

	/**
	 * Verifica se o metodo eh um getter
	 * 
	 * @param method
	 * @return
	 */
	private boolean isGetter(Method method) {
		if (!method.getName().startsWith("get")) {
			return false;
		}
		if (method.getParameterTypes().length != 0) {
			return false;
		}
		if (void.class.equals(method.getReturnType())) {
			return false;
		}
		return true;
	}

}